Skip to content

Use lazy property initialization in generated C# RPC classes#725

Merged
stephentoub merged 1 commit intomainfrom
stoub/lazyinitprops
Mar 10, 2026
Merged

Use lazy property initialization in generated C# RPC classes#725
stephentoub merged 1 commit intomainfrom
stoub/lazyinitprops

Conversation

@stephentoub
Copy link
Collaborator

@stephentoub stephentoub commented Mar 8, 2026

The C# codegen was emitting eager initialization for collection and nested-class properties:

public List<Tool> Tools { get; set; } = [];
public ModelCapabilities Capabilities { get; set; } = new();

During deserialization, this allocates an empty collection/object only to immediately overwrite it. This PR changes the codegen to use lazy initialization via f ield keyword instead:

public List<Tool> Tools { get => field ??= []; set; }
public ModelCapabilities Capabilities { get => field ??= new(); set; }

This avoids unnecessary allocations when the property is set by the deserializer, while still ensuring a non-null default when accessed without prior assignment.

Changes

  • **\scripts/codegen/csharp.ts**: Updated property emission for \List<>/\Dictionary<>\ and nested RPC class types to use f ield ??=\ lazy init.
  • **\dotnet/src/Generated/Rpc.cs**: Regenerated with the new pattern.

@stephentoub stephentoub requested a review from a team as a code owner March 8, 2026 14:41
Copilot AI review requested due to automatic review settings March 8, 2026 14:41
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

Cross-SDK Consistency Review ✅

This PR modifies the C# code generator to use lazy property initialization (field ??=) instead of eager initialization for collections and nested objects. I've reviewed the equivalent code in the other SDK implementations:

Language-Specific Analysis

TypeScript (nodejs/src/generated/rpc.ts):

  • Uses plain TypeScript interfaces with no initialization
  • No eager allocation issue exists

Python (python/copilot/generated/rpc.py):

  • Uses @dataclass with explicit field type annotations
  • Python's dataclass mechanism handles initialization differently
  • No equivalent allocation overhead

Go (go/rpc/generated_rpc.go):

  • Uses structs with zero-value semantics
  • Slices and maps are nil by default, structs are zero-valued
  • No eager allocation occurs

Conclusion

No cross-SDK changes needed. This optimization is C#-specific and addresses a performance concern unique to how C# handles property initialization during JSON deserialization. The eager initialization pattern (= [], = new()) causes unnecessary allocations when the deserializer immediately overwrites these values.

The other languages handle property initialization fundamentally differently:

  • TypeScript/JavaScript objects are dynamic with no initialization overhead
  • Python dataclasses use a different initialization model
  • Go's zero-value semantics provide "free" default values

This is an appropriate language-specific optimization that correctly does not apply to the other SDK implementations.

Generated by SDK Consistency Review Agent for issue #725 ·

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the C# code generator and regenerated RPC DTOs to avoid eager allocations for required collection and nested RPC-class properties by switching to lazy initialization using the field ??= pattern.

Changes:

  • Adjusted scripts/codegen/csharp.ts to emit lazy-initialized accessors for required List<>/Dictionary<> and nested RPC class properties.
  • Regenerated dotnet/src/Generated/Rpc.cs to apply the new accessor pattern to affected properties.

Reviewed changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 2 comments.

File Description
scripts/codegen/csharp.ts Changes RPC property emission to generate lazy-initialized accessors for certain required reference types.
dotnet/src/Generated/Rpc.cs Regenerated RPC DTOs reflecting the new lazy-init accessor pattern.

@stephentoub stephentoub enabled auto-merge March 8, 2026 14:50
Switched property initializations to lazy accessors for lists, dictionaries, and custom types in C# RPC classes. Updated codegen in csharp.ts to emit these accessors, improving memory usage and consistency.
@stephentoub stephentoub force-pushed the stoub/lazyinitprops branch from 8547a85 to 4e35e0b Compare March 10, 2026 02:20
@github-actions
Copy link
Contributor

Cross-SDK Consistency Review ✅

I've reviewed this PR for consistency across all four SDK implementations (Node.js/TypeScript, Python, Go, and .NET).

Summary

No consistency concerns. This PR implements a C#-specific performance optimization that doesn't require equivalent changes in other SDKs.

Analysis

The PR changes C# RPC class codegen from eager initialization:

public List(Model) Models { get; set; } = [];
public ModelCapabilities Capabilities { get; set; } = new();

To lazy initialization using C# 13's field keyword:

public List(Model) Models { get => field ??= []; set; }
public ModelCapabilities Capabilities { get => field ??= new(); set; }

This avoids allocating empty collections/objects during construction that are immediately overwritten by the JSON deserializer—a C#-specific issue.

Why Other SDKs Don't Need This

The other three SDKs don't have this allocation inefficiency:

  • TypeScript: Uses plain interfaces with no runtime initialization at all
  • Python: Uses dataclass + from_dict() pattern where quicktype-generated code initializes fields directly from the deserialized dictionary
  • Go: Uses structs with zero values; JSON unmarshaling directly populates fields without pre-allocation

Each language's codegen already handles deserialization efficiently given that language's idioms and runtime behavior.

Conclusion

This optimization is appropriate and maintains cross-SDK semantic consistency (all SDKs correctly deserialize RPC types), while taking advantage of C#-specific features to improve performance. No changes needed in other SDKs. 🎯

Generated by SDK Consistency Review Agent for issue #725 ·

@stephentoub stephentoub added this pull request to the merge queue Mar 10, 2026
Merged via the queue into main with commit 7235609 Mar 10, 2026
27 checks passed
@stephentoub stephentoub deleted the stoub/lazyinitprops branch March 10, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants